/*
 * Copyright 2008-2023 dexian.vip. All rights reserved.
 * Support: http://www.dexian.vip
 * License: http://www.dexian.vip/license
 */

package vip.dexian.admin.log;

import cn.hutool.core.util.NumberUtil;
import vip.dexian.admin.dto.AdminPrincipal;
import vip.dexian.admin.entity.OperateLog;
import vip.dexian.admin.enums.OperateTypeEnum;
import vip.dexian.admin.service.AdminService;
import vip.dexian.admin.service.OperateLogService;
import vip.dexian.common.utils.IpToLongUtils;
import vip.dexian.common.utils.JsonUtils;
import vip.dexian.common.utils.UserAgentUtils;
import vip.dexian.common.utils.WebUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * 管理员操作日志切面
 *
 * @author 挺好的 2023年06月07日 11:31
 */
@Aspect
@Component
public class AdminOperateLogAspect {

    /**
     * 不保存日志的属性
     */
    private static final String[] IGNORE_PROPERTIES = new String[] {"password", "rePassword", "oldPassword", "newPassword", "_csrf", "file", "redirectUrl"};

    /**
     * 管理员
     */
    @Resource (name = "adminServiceImpl")
    private AdminService adminService;

    /**
     * 操作日志
     */
    @Resource (name = "operateLogServiceImpl")
    private OperateLogService operateLogService;

    /**
     * 记录开始时间和结束时间
     */
    private final ThreadLocal <Long> startTimeThreadLocal = new NamedThreadLocal <>("Log startTime");

    /**
     * 开启之前
     *
     * @param joinPoint
     *
     * @throws Throwable
     */
    @Before (value = "logPointCut()")
    public void before (JoinPoint joinPoint) {
        this.startTimeThreadLocal.set(System.currentTimeMillis());
    }

    /**
     * 设置操作日志切入点 记录操作日志 在注解的位置切入代码
     */
    @Pointcut ("@annotation(vip.dexian.admin.log.AdminLog)")
    public void logPointCut () {
    }

    /**
     * 设置操作异常切入点记录异常日志 扫描所有controller包下操作
     */
    @Pointcut ("execution(* vip.dexian.admin.controller..*.*(..))")
    public void exceptionLogPointCut () {
    }


    /**
     * 正常返回通知，拦截用户操作日志，连接点正常执行完成后执行， 如果连接点抛出异常，则不会执行
     *
     * @param joinPoint
     *         切入点
     * @param keys
     *         返回结果
     */
    @AfterReturning (value = "logPointCut()", returning = "keys")
    public void saveLog (JoinPoint joinPoint, Object keys) {
        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(
                RequestAttributes.REFERENCE_REQUEST);

        if (request == null) {
            return;
        }

        OperateLog operateLog = new OperateLog();

        AdminLog log = this.addMethodAndValid(joinPoint, operateLog);

        // 如果log注解为空
        if (log == null) {
            return;
        }

        // 解析浏览器信息
        this.addUserAgentInfo(request, operateLog);
        // 添加请求响应参数
        this.addRequestResponse(request, operateLog, keys, log);
        // 添加操作人员
        this.addOperator(operateLog);
        // 添加可删除时间、方法执行时间等
        this.addOther(operateLog, request);

        this.operateLogService.log(operateLog);

    }

    /**
     * 添加可删除时间、方法执行时间等
     *
     * @param operateLog
     *         操作日志
     * @param request
     *         {@link HttpServletRequest }
     */
    private void addOther (OperateLog operateLog, HttpServletRequest request) {
        // 开始时间
        Long startTime = this.startTimeThreadLocal.get();
        // 结束时间
        Long currentTime = System.currentTimeMillis();

        operateLog.setOperateUrl(request.getRequestURI());
        operateLog.setBeginTimeMillis(startTime);
        operateLog.setEndTimeMillis(currentTime);

        // 及时清除数据，避免内存泄露
        this.startTimeThreadLocal.remove();
    }

    /**
     * 添加操作员
     *
     * @param operateLog
     *         {@link OperateLog}
     */
    private void addOperator (OperateLog operateLog) {
        AdminPrincipal admin = this.adminService.getCurrent();
        operateLog.setAdminId(admin.getId());
        operateLog.setUsername(admin.getUsername());
    }

    /**
     * 设置浏览器解析的信息
     *
     * @param request
     *         {@link HttpServletRequest }
     * @param operateLog
     *         操作日志
     */
    private void addUserAgentInfo (HttpServletRequest request, OperateLog operateLog) {
        UserAgentUtils.UserAgentInfo userAgentInfo = UserAgentUtils.parse(request);

        operateLog.setIp(IpToLongUtils.toLong(WebUtils.getIPAddr(request)));
        operateLog.setBrowser(userAgentInfo.getBrowser());
        operateLog.setBrowserVersion(userAgentInfo.getBrowserVersion());
        operateLog.setOsName(userAgentInfo.getOsName());
        operateLog.setDeviceType(userAgentInfo.getDeviceType());
    }

    /**
     * 添加方法相关
     *
     * @param joinPoint
     *         连接点
     * @param operateLog
     *         操作日志
     *
     * @return 返回Log注解
     */
    private AdminLog addMethodAndValid (JoinPoint joinPoint, OperateLog operateLog) {
        // 从切面织入点处通过反射机制获取织入点处的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取切入点所在的方法
        Method method = signature.getMethod();
        // 获取操作
        AdminLog log = method.getAnnotation(AdminLog.class);

        if (log == null) {
            return null;
        }

        String module = log.module();
        OperateTypeEnum type = log.type();
        String description = log.description();

        // 获取请求的类名
        String className = joinPoint.getTarget().getClass().getName();
        // 获取请求的方法名
        String methodName = method.getName();
        methodName = className + "." + methodName;

        // 设置请求方法
        operateLog.setMethod(methodName);
        operateLog.setOperateType(type);
        operateLog.setDescription(description);
        operateLog.setModule(module);

        return log;
    }

    /**
     * 添加请求参数和响应参数
     *
     * @param request
     *         {@link HttpServletRequest}
     * @param operateLog
     *         {@link OperateLog}
     * @param keys
     *         返回值
     * @param log
     *         log注解
     */
    private void addRequestResponse (HttpServletRequest request, OperateLog operateLog, Object keys, AdminLog log) {
        // 请求的参数
        Map <String, Object> requestParameters = this.convertMap(request.getParameterMap());

        if (MapUtils.isNotEmpty(requestParameters)) {
            operateLog.setRequest(JsonUtils.toJson(requestParameters));
        }

        Long updateId = 0L;
        if (OperateTypeEnum.UPDATE == log.type()) {
            updateId = this.getId(requestParameters);

        }
        operateLog.setUpdateId(updateId);
    }

    /**
     * 转换request 请求参数
     *
     * @param paramMap
     *         request获取的参数数组
     */
    public Map <String, Object> convertMap (Map <String, String[]> paramMap) {
        Map <String, Object> resultMap = new HashMap <>();

        for (Map.Entry <String, String[]> entry : paramMap.entrySet()) {

            String key = entry.getKey();

            if (ArrayUtils.contains(IGNORE_PROPERTIES, key)) {
                continue;
            }

            String[] values = entry.getValue();

            if (ArrayUtils.isEmpty(values)) {
                continue;
            }

            if (values.length == 1) {
                resultMap.put(key, values[0]);
            } else {
                resultMap.put(key, Arrays.asList(values));
            }
        }
        return resultMap;
    }

    /**
     * 获取id
     *
     * @param parameterMap
     *         参数Map
     *
     * @return 如果id不存在返回0
     */
    private Long getId (Map <String, Object> parameterMap) {

        Object id = parameterMap.get("id");

        if (id == null) {
            return 0L;
        }

        try {
            // 该方法可能抛出异常
            return NumberUtil.parseLong(id.toString());
        } catch (Exception e) {
            return 0L;
        }
    }

}
